OilTrack · ภาพรวมระบบ (System overview) Prototype V.2 · 2026-09
Spheresoft Co., Ltd. เอกสารอ้างอิงต้นแบบ — ไม่ใช่สัญญาส่งมอบ

OilTrack · ภาพรวมระบบ

ระบบควบคุมการเบิก-จ่ายน้ำมันเชื้อเพลิงยานพาหนะ ที่ผูก การอนุมัติ (approval) เข้ากับ หัวจ่ายจริง (dispenser head) ด้วยอุปกรณ์ IoT — เอกสารนี้อธิบายว่าระบบมีอะไร ใครใช้ส่วนไหน ข้อมูลอยู่ที่ใด และกฎอะไรที่ทุกหน้าจอต้องเคารพร่วมกัน

ผู้อ่าน: ลูกค้าที่อนุมัติ business flow · ทีมพัฒนาที่จะสร้างระบบจริง · ทีม QA ที่เขียนแผนทดสอบ · ทีมงานใหม่ที่ต้องเข้าใจภาพรวม อ่านคู่กับ Doc - Business Flow (ลำดับงานตั้งแต่ต้นจนจบ) และ Doc - Kiosk Gateway (state machine ของตู้หัวจ่าย)

1 · แนวคิดหลักและสามพื้นผิวการใช้งาน

หัวใจของระบบคือประโยคเดียว: การอนุมัติเป็นเพียงข้อความ จนกระทั่งถูกผูกกับฮาร์ดแวร์ คำขอที่อนุมัติแล้วยังจ่ายน้ำมันไม่ได้ ต้องมีการผูก (bind) คำขอนั้นเข้ากับหัวจ่ายจริงที่ตู้คีออสก์ก่อน ทุกหน้าจอต้องทำให้ความต่างนี้อ่านได้ทันที

Application · Backoffice
หลังบ้าน
ผู้ดูแลระบบจัดการข้อมูลหลักและอุปกรณ์ · พนักงานบัญชีอนุมัติคำขอ มอบหมายงานจัดส่ง คุมสต็อกและต้นทุน
desktop · 1280px ขึ้นไป
Application · Driver
แอปคนขับรถ
คนขับยื่นคำขอเบิกน้ำมัน ติดตามสถานะ สแกน QR ที่หัวจ่าย และยืนยันการผูกในแอป
mobile web (PWA) · 375–430px
Application · Gateway Kiosk
ตู้คีออสก์ที่หัวจ่าย
จอที่ติดตั้งกับหัวจ่าย แสดง QR + รหัส 6 หลัก มาตรวัดสด และใบสรุป — อ่านอย่างเดียว ไม่มีปุ่มให้กด
7" panel · 1024 × 600 คงที่
รูปที่ 1 — สามพื้นผิวการใช้งาน ทั้งสามอ่าน-เขียนฐานข้อมูลชุดเดียวกัน การเปลี่ยนแปลงจากพื้นผิวใดก็เห็นในอีกสองพื้นผิวทันที

ฐานข้อมูลร่วมและการเห็นข้อมูลตรงกัน (shared store)

ต้นแบบนี้ใช้ store ในเบราว์เซอร์ตัวเดียว (oiltrack-db.js + oiltrack-seed.json) เป็นแหล่งความจริงเดียว ทุกหน้าจอ subscribe การเปลี่ยนแปลงและ re-render เอง ไม่มีหน้าไหนถือข้อมูลตัวอย่างของตัวเอง เมื่อระบบจริงมาแทน จุดต่อคือ API layer ที่ให้ฟังก์ชันชุดเดียวกันนี้

WRITE
พนักงานบัญชีกดอนุมัติในหลังบ้าน
transact() → notify
BroadcastChannel + storage event
RE-RENDER
แอปคนขับเห็นสถานะ “อนุมัติแล้ว” · คีออสก์เปิดรับการสแกน
รูปที่ 2 — เส้นทางการแจ้งเตือนข้ามหน้าจอ การเขียนหนึ่งครั้งคือหนึ่ง localStorage write และหนึ่งการแจ้งเตือน ทุกแท็บที่เปิดอยู่จะ re-render เงียบ ๆ โดยไม่มีเอฟเฟกต์ (ค่าบนจอเปลี่ยนเอง)

2 · ผู้ใช้งานและสิทธิ์ (actors & permissions)

ผู้ใช้งาน เข้าสู่ระบบด้วย ทำอะไรได้ ทำไม่ได้
ผู้ดูแลระบบ
administrator
อีเมล + รหัสผ่าน ข้อมูลหลักทั้งหมด (รถ · สถานี · ถัง · ตู้จ่าย · สถานที่หน้างาน) · ผู้ใช้งานทุกบทบาท · มอบหมายรถ↔คนขับ · สั่งเปิด/ปิดการจ่ายของหัวจ่าย อนุมัติคำขอ · บันทึกสต็อก
พนักงานบัญชี
accountant
อีเมล + รหัสผ่าน อนุมัติ/ไม่อนุมัติคำขอ · แก้ลิตรที่อนุมัติ · ต่อเวลา · ปลดหัวจ่าย · ยกเลิกคำขอ · มอบหมายคำขอจัดส่ง · บันทึกสต็อกและปรับปรุงยอด · ตรวจสอบธงระยะทาง · รายงาน แก้ข้อมูลหลัก · เพิ่ม/ลบผู้ใช้งาน
คนขับรถ
driver
เบอร์โทร + OTP ยื่นคำขอสำหรับรถที่ตนรับผิดชอบ · ยกเลิกคำขอของตน · สแกน QR ที่หัวจ่ายและยืนยันการผูก · ดูประวัติของตน เห็นความจุถังเป็นตัวเลข · เห็นธงระยะทางหรือผลตรวจสอบ · เห็นคำขอของคนอื่น · อนุมัติคำขอของตน
ตู้คีออสก์
gateway (อุปกรณ์)
ผูกกับหัวจ่ายด้วย gatewayId แสดง QR/รหัส · จอง (reserve) หัวจ่ายเมื่อสแกน · เดินมาตรวัดและปิดงานเมื่อครบเพดานหรือหยุดจ่ายครบเวลา รับ input จากคนที่หน้าตู้ (อ่านอย่างเดียว) · ยกเลิกคำขอ
ตารางที่ 1 — สิทธิ์ตามบทบาท ในต้นแบบนี้บทบาทหลังบ้านสองบทบาทอยู่ในไฟล์เดียว สลับด้วยตัวสลับบทบาทบนหัวหน้า (role switcher) เพื่อสาธิตได้ทั้งสองฝั่งโดยไม่ต้องออกจากระบบ
กฎที่ห้ามละเมิด: คนขับต้องไม่เห็นความจุถังของรถเป็นจำนวนลิตร ระบบตรวจเพดานให้เงียบ ๆ และแสดงเป็นสถานะ (เต็มถัง · ติดตั้งถังเคลื่อนที่) เท่านั้น — ใช้กับทุกหน้าจอฝั่งคนขับ

3 · แผนผังหน้าจอ (screen map)

Backoffice
ผู้ดูแลระบบ
รถยนต์ สถานีบริการ ถังน้ำมัน ตู้จ่ายน้ำมัน สถานที่หน้างาน มอบหมายรถให้คนขับ มอบหมายคนขับให้รถ คนขับรถ พนักงานบัญชี ผู้ดูแลระบบ ตั้งค่าระบบ · บันทึกระบบ · เซิร์ฟเวอร์ควบคุม (ยังไม่พัฒนา)
Backoffice
พนักงานบัญชี
รายงานคำขอ รายการที่รออนุมัติ มอบหมายคำขอจัดส่ง รายการคำขอ รายงานสต็อก สต็อกปัจจุบัน รายการบันทึกสต็อก รายการล็อตสต็อก รายการใช้งานสต็อก
Driver
คนขับรถ
เข้าสู่ระบบ (เบอร์ + OTP) คำขอของฉัน สร้างคำขอใหม่ (4 ขั้น) ผูกหัวจ่าย · สแกน/กรอกรหัส งานจัดส่ง บัญชีของฉัน · สิทธิ์การใช้งาน บัญชีถูกปิดใช้งาน
Gateway Kiosk
อุปกรณ์
#0 boot #1 idle #2 binding #3 bound #4 progress #5 complete #6 aborted #7 degraded #8 locked #9 error #10 recovery
เครื่องมือ
tools
Tool - Database · ตาราง / Raw JSON / Checks Tool - Test Case · ST / GEO / KIOSK
รูปที่ 3 — ทุกหน้าจอในระบบ แยกตามพื้นผิว กรอบสีน้ำเงินคือหน้าจอที่อยู่บนเส้นทางหลักของการเบิก-จ่าย เส้นประคือหน้าที่ยังไม่พัฒนา

4 · โครงสร้างข้อมูล (data model)

ทั้งระบบมี 16 ตาราง แบ่งเป็นสามชั้น: ข้อมูลหลัก (คนกรอก) · รายการเดินเรื่อง (เกิดจากงานประจำวัน) · สต็อกที่ระบบสร้างเอง (ไม่มีหน้าจอไหนเขียนตรง)

ตาราง คีย์ คืออะไร ใครเขียน
ข้อมูลหลัก · master data
administratorsemailผู้ดูแลระบบผู้ดูแลระบบ
accountantsemailพนักงานบัญชีผู้ดูแลระบบ
driversphoneNoคนขับ + สถานะลงทะเบียน/OTPผู้ดูแลระบบ · OTP โดยระบบ
carslicenseNoรถ · ชนิดเชื้อเพลิง · ความจุ · คนขับที่รับผิดชอบ · tankId เมื่อติดถังเคลื่อนที่ผู้ดูแลระบบ
stations · worksitesid · codeสถานีบริการ และสถานที่หน้างาน (พร้อมพิกัดอ้างอิงสำหรับธงระยะทาง)ผู้ดูแลระบบ
tanksidถัง ประจำสถานี (site) หรือ ติดรถ (mobile) + ความจุผู้ดูแลระบบ
dispensersid · codeหัวจ่าย + หัวจ่ายย่อย (nozzles) · gatewayId · dispenseEnabledผู้ดูแลระบบ
รายการเดินเรื่อง · transactional
requestsREQ-nnnnคำขอเบิกน้ำมัน — สถานะ · ลิตรที่ขอ/อนุมัติ/จ่ายจริง · หัวจ่ายที่ผูก · กำหนดเวลา · geoPoints · หมายเหตุภายใน · ต้นทุนคนขับ · บัญชี · คีออสก์
dispatchRequestsMD-nnnnคำขอจัดส่ง — รวมคำขอหลายใบเข้ารถน้ำมันเคลื่อนที่หนึ่งคัน + คำขอเติมถัง (top-up)บัญชี
stockRecordsSR-nnnnรายการบันทึกสต็อก — incoming / add / deduct ชนิดรายการเป็นตัวกำหนดทิศทางบัญชี · ระบบ (การจ่าย)
devices · sweepLog · session · configสิทธิ์บนเครื่องคนขับ · บันทึกการกวาดล่าสุด 50 รายการ · เซสชันที่เข้าสู่ระบบ · ค่าคอนฟิกและตัวนับเลขที่ระบบ
สต็อกที่ระบบสร้างเอง · engine-owned (อ่านเท่านั้น)
stockLotsLOT-nnnnคิว FIFO หนึ่งล็อตต่อหนึ่งรายการฝั่งเพิ่ม — ที่เดียวที่เก็บต้นทุนต่อลิตรstock engine
stockUsagesSU-nnnnหนึ่งแถวต่อการตัดสต็อกหนึ่งครั้ง พร้อมการปันส่วนรายล็อต ต้นทุนรวม และบริบทของคำขอstock engine
liveStocktankIdยอดคงเหลือปัจจุบันที่คำนวณไว้แล้ว — หน้าจออ่านค่าเดียวจบ ไม่ต้องรวมประวัติstock engine
ตารางที่ 2 — ตารางข้อมูลทั้งหมด สามตารางล่างเป็นผลลัพธ์ที่ระบบสร้างจากประวัติ ลบและสร้างใหม่ได้เสมอด้วย rebuildStock()

5 · สถานะคำขอและการล็อกทรัพยากร

คำขอมี 10 สถานะ แบ่งเป็นสามกลุ่ม: มีชีวิต (live — ยังถือทรัพยากรอยู่) · ผูกฮาร์ดแวร์ (bound — ถือหัวจ่ายด้วย) · ปิดเรื่องแล้ว (terminal) หน้าจอต้องอ่านจาก DB.statusMeta() เท่านั้น ห้ามเขียนรายการสถานะเองในหน้าจอ

รออนุมัติ
awaiting-review
ล็อกรถ + คนขับ
รอมอบหมายงาน
awaiting-assign
ล็อกรถ + คนขับ
อนุมัติแล้ว
approved
ล็อกรถ + คนขับ · นับเวลาอนุมัติ
รอยืนยันการผูก
binding
+ จองหัวจ่าย · 2 นาที
ผูกหัวจ่ายแล้ว
bound
+ ถือหัวจ่าย · 5 นาที
กำลังเติมน้ำมัน
fueling
+ ถือหัวจ่าย · ไม่หมดอายุเอง
เสร็จสิ้น
completed
ปล่อยทุกอย่าง · ตัดสต็อก
ไม่อนุมัติ · ยกเลิก
rejected · cancelled
ปล่อยทุกอย่าง · ไม่ตัดสต็อก
หมดอายุ
expired
ระบบกวาด · ไม่ตัดสต็อก
ผิดพลาด
error
ตัดสต็อกเท่าที่จ่ายไปแล้ว
รูปที่ 4 — สถานะทั้งสิบและสิ่งที่แต่ละสถานะถือไว้ สีเดียวกับที่ใช้บนหน้าจอทุกหน้า

การล็อกสามชั้น

รถ — คำขอที่มีชีวิตหนึ่งใบล็อกรถคันนั้นสำหรับทุกคน ตั้งแต่รออนุมัติเป็นต้นไป รถที่ถูกล็อกยังแสดงในรายการของคนขับแต่เป็นสีเทาพร้อมเหตุผล ไม่ซ่อน · คนขับ — หนึ่งคำขอที่มีชีวิตต่อคน · หัวจ่าย — หนึ่งการผูกต่อหัว การผูกซ้ำถูกปฏิเสธด้วยเหตุผล dispenser-in-use การปล่อยทุกกรณีไปทางเดียวคือ releaseRequest() ซึ่งล้างการผูก กำหนดเวลา และช่องคำขอจัดส่งในการเขียนครั้งเดียว ยกเลิกแล้วคนขับยื่นใหม่ได้ทันที

6 · เครื่องคิดสต็อกแบบ FIFO

น้ำมันที่รับเข้าแต่ละครั้งเปิดเป็น ล็อต ที่มีต้นทุนต่อลิตรของตัวเอง การจ่ายออกตัดจากล็อตเก่าที่สุดก่อน และการจ่ายครั้งเดียวอาจข้ามหลายล็อต — ต้นทุนของคำขอนั้นจึงเป็นค่าเฉลี่ยถ่วงน้ำหนักตามที่ตัดจริง

คิว FIFO ของถัง
LOT-0021
คงเหลือ 40 ล. · ฿29.50
LOT-0022
คงเหลือ 500 ล. · ฿30.20
LOT-0023 …
จ่ายจริง 70.00 ลิตร → ตัดสองล็อต
40.00 ล. @ ฿29.50
30.00 ล. @ ฿30.20
ต้นทุนรวม
฿2,086.00
เฉลี่ย/ลิตร
฿29.80
จำนวนล็อต
2
ค่าคงที่ที่ต้องเป็นจริงเสมอ: liveStock.litres = Σ รายการที่มีเครื่องหมาย = Σ ลิตรคงเหลือของทุกล็อต — ตรวจได้ที่แท็บ Checks ของ Tool - Database
รูปที่ 5 — การปันส่วนต้นทุนแบบ FIFO ของการจ่ายหนึ่งครั้ง ตัวเลขนี้คือรูปแบบที่หน้ารายการใช้งานสต็อกแสดงในแผงขวา

สามกรณีที่ต้องเข้าใจ: สต็อกไม่พอ — ยังจ่ายได้ ส่วนที่เกินคิดต้นทุนด้วยราคาล่าสุดที่รู้ และแถวนั้นถูกตั้งธง ต้นทุนไม่ครบ ให้บัญชีตามแก้ด้วยการรับเข้าย้อนหลัง · โอนเข้ารถน้ำมันเคลื่อนที่ — ต้นทุนติดไปกับน้ำมัน ล็อตบนรถจึงไม่ใช่ ฿0 · การจ่ายตามคำขอ — เป็นชนิดรายการที่ระบบเขียนเองเท่านั้น ไม่มีในตัวเลือกของฟอร์ม และไม่ปนอยู่ในหน้ารายการบันทึกสต็อก

7 · กำหนดเวลาและการกวาดอัตโนมัติ

ค่าคอนฟิก ค่า นับจากอะไร หมดเวลาแล้วเกิดอะไร
reviewExpiryMinutes240คนขับยื่นคำขอหมดอายุ · ปล่อยรถและคนขับ
approvalExpiryMinutes120อนุมัติ (และเริ่มใหม่เมื่อส่งงานจัดส่ง)หมดอายุ · ปล่อยรถและคนขับ
totpWindowSeconds30นาฬิกาของหัวจ่ายรหัส 6 หลักเปลี่ยน ไม่มีช่วงผ่อนผัน
bindConfirmMinutes2สแกนสำเร็จ (จองหัวจ่าย)กลับเป็น อนุมัติแล้ว · ปลดหัวจ่าย
fuelStartMinutes5ยืนยันการผูกในแอปยกเลิก คำขอ · ปลดหัวจ่าย
fuelIdleTimeoutMinutes1หยุดจ่ายกลางคันปิดงานด้วยลิตรที่จ่ายจริง → เสร็จสิ้น
geoFlagRadiusMeters500พิกัดอ้างอิงของแต่ละเหตุการณ์ตั้งธงให้บัญชีตรวจ — ไม่บล็อกคนขับ
ตารางที่ 3 — กำหนดเวลาทั้งหมดมาจาก config ไม่มีค่าคงที่ในหน้าจอ ผลของ binding และ bound ต่างกันโดยเจตนา: อย่างแรกยังไม่ได้จับฮาร์ดแวร์จึงถอยกลับได้ อย่างหลังถือหัวจ่ายไว้แล้วจึงต้องปิดเรื่อง

การบังคับใช้ทำสองทางโดยตั้งใจ: ตู้คีออสก์บังคับสด เพราะเป็นเจ้าของหน้าจอ และ DB.sync() เป็นตัวสำรองสำหรับตู้ที่ออฟไลน์ รีบูต หรือไม่ได้เปิดอยู่ ทั้งสองทางเรียกฟังก์ชันเดียวกันจึงได้ผลเหมือนกัน — และเรียกซ้ำได้ไม่เปลี่ยนผล (idempotent) นอกจากกวาดหมดอายุ sync() ยังปิดคำขอจัดส่งที่คำขอในนั้นจบหรือถูกปล่อยหมดแล้ว และเขียนเครดิตน้ำมันเข้าถังเคลื่อนที่เมื่อคำขอเติมถังเสร็จ

8 · ศัพท์ที่ใช้ (glossary)

ไทย English / code ความหมาย
คำขอเบิกน้ำมันrequest · REQ-nnnnหน่วยงานหลักของระบบ หนึ่งใบ = การเติมหนึ่งครั้ง
ผูกหัวจ่ายbindจับคู่คำขอที่อนุมัติแล้วกับหัวจ่ายจริง สองขั้น: สแกน → ยืนยันในแอป
ปลดหัวจ่ายunbindบัญชีคืนหัวจ่ายให้ว่าง คำขอกลับไปรอผูกใหม่
หัวจ่าย / หัวจ่ายย่อยdispenser · nozzleตู้จ่ายหนึ่งตู้มีหัวจ่ายย่อย 1–4 หัว แต่ละหัวผูกกับถังและชนิดน้ำมันหนึ่งอย่าง
รถน้ำมันเคลื่อนที่mobile fuel truckรถที่ติดถังและหัวจ่าย ออกไปเติมให้เครื่องจักรที่หน้างาน
คำขอจัดส่งdispatch · MD-nnnnงานหนึ่งรอบของรถน้ำมันเคลื่อนที่ รวมคำขอหลายใบ + คำขอเติมถังของตัวรถ
เติมถังเคลื่อนที่top-upคำขอที่ระบบสร้างให้คนขับรถน้ำมัน เพื่อเติมถังบนรถก่อนออกงาน (0 ลิตร = ไม่ต้องเติม)
ล็อตสต็อกlot · LOT-nnnnน้ำมันหนึ่งชุดที่รับเข้าพร้อมต้นทุนต่อลิตรของมัน
รายการใช้งานสต็อกusage · SU-nnnnการตัดสต็อกหนึ่งครั้ง พร้อมรายละเอียดว่าตัดจากล็อตไหนเท่าไร
ธงระยะทางdistance flagพิกัดที่ห่างจากจุดอ้างอิงเกินรัศมี — เป็นคำขอให้ตรวจสอบ ไม่ใช่การบล็อก
การกวาดsweep · DB.sync()รอบตรวจที่ปิดคำขอหมดอายุ ปิดงานจัดส่งที่ว่างเปล่า และเขียนเครดิตที่ค้าง
รหัส 6 หลักที่หัวจ่ายTOTPรหัสที่หมุนทุก 30 วินาที ใช้แทนการสแกน QR เมื่อกล้องใช้ไม่ได้
ตารางที่ 4 — คำที่ใช้ตรงกันทุกที่ ทั้งบนหน้าจอ ในเอกสาร และในโค้ด

9 · สิ่งที่ยังไม่ได้ทำ และคำถามที่ต้องตัดสินใจ

ยังไม่พัฒนาในต้นแบบ
ตั้งค่าระบบ · บันทึกระบบ (audit log ที่ผู้ใช้อ่านได้) · หน้าเซิร์ฟเวอร์ควบคุม · การแจ้งเตือนจริง (push/SMS) · การนำเข้าไฟล์ Excel จริง (ปุ่มมีแล้ว ยังไม่ประมวลผลไฟล์) · การพิมพ์ใบสรุปที่หัวจ่าย · รายงานส่งออกรูปแบบอื่นนอกจาก CSV
ต้องการการตัดสินใจ
ใครมีสิทธิ์แก้ลิตรที่อนุมัติหลังผูกหัวจ่ายแล้ว · เก็บประวัติคำขอย้อนหลังกี่ปี · นโยบายเมื่อสต็อกติดลบจากการนับจริง · จำนวนครั้งที่กรอกรหัสผิดก่อนล็อกบัญชี (หน้าจอมีสถานะแล้ว ยังไม่มีกฎ) · จะให้คนขับเห็นเหตุผลการไม่อนุมัติแบบเต็มหรือแบบย่อ
ตารางที่ 5 — ช่องว่างที่รู้ตัว เขียนไว้ตรง ๆ ดีกว่าปล่อยให้เข้าใจว่าครบแล้ว